Backup workflows to git repository on Gitea

工作流概述

这是一个包含20个节点的复杂工作流,主要用于自动化处理各种任务。

工作流源代码

下载
{
  "id": "Ef2uEM6H19K2DGUO",
  "meta": {
    "templateId": "2532",
    "templateCredsSetupCompleted": true
  },
  "name": "Backup workflows to git repository on Gitea",
  "tags": [
    {
      "id": "UWNX4AzSneYNvTQI",
      "name": "Gitea",
      "createdAt": "2025-01-28T23:10:06.823Z",
      "updatedAt": "2025-01-28T23:10:06.823Z"
    },
    {
      "id": "4b7Bs9T0Cagsg5tT",
      "name": "Git",
      "createdAt": "2025-01-28T23:10:26.545Z",
      "updatedAt": "2025-01-28T23:10:26.545Z"
    },
    {
      "id": "HiN3ehC2KkAp5kVs",
      "name": "Backup",
      "createdAt": "2025-01-28T23:10:38.878Z",
      "updatedAt": "2025-01-28T23:10:38.878Z"
    }
  ],
  "nodes": [
    {
      "id": "639582ef-f13e-4844-bd10-647718079121",
      "name": "Globals",
      "type": "n8n-nodes-base.set",
      "position": [
        600,
        240
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "repo.url",
              "value": "https://git.vdm.dev"
            },
            {
              "name": "repo.name",
              "value": "workflows"
            },
            {
              "name": "repo.owner",
              "value": "n8n"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "9df89713-220e-43b9-b234-b8f5612629cf",
      "name": "n8n",
      "type": "n8n-nodes-base.n8n",
      "position": [
        840,
        240
      ],
      "parameters": {
        "filters": {},
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "id": "ZjfxOLTTHX2CzbKa",
          "name": "Main N8N Account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4b2d375c-a339-404c-babd-555bd2fc4091",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        380,
        240
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 45
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ea026e96-0db1-41fd-b003-2f2bf4662696",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2620,
        300
      ],
      "parameters": {
        "height": 80,
        "content": "Workflow changes committed to the repository"
      },
      "typeVersion": 1
    },
    {
      "id": "9c402daa-6d03-485d-b8a0-58f1b65d396d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2260,
        180
      ],
      "parameters": {
        "height": 80,
        "content": "Check if there are any changes in the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "1d9216d9-bf8d-4945-8a58-22fb1ffc9be8",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1800,
        580
      ],
      "parameters": {
        "height": 80,
        "content": "Create a new file for the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "60a3953b-d9f1-4afd-b299-e314116b96c6",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1300,
        200
      ],
      "parameters": {
        "height": 80,
        "content": "Check if file exists in the repository"
      },
      "typeVersion": 1
    },
    {
      "id": "f2340ad0-71a1-4c74-8d90-bcb974b8b305",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        780,
        180
      ],
      "parameters": {
        "height": 80,
        "content": "Get all workflows"
      },
      "typeVersion": 1
    },
    {
      "id": "617bea19-341a-4e9d-b6fd-6b417e58d756",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        500,
        180
      ],
      "parameters": {
        "height": 80,
        "content": "Set variables"
      },
      "typeVersion": 1
    },
    {
      "id": "72f806d7-e30a-470b-9ba2-37fdc35de3c8",
      "name": "SetDataUpdateNode",
      "type": "n8n-nodes-base.set",
      "position": [
        1920,
        240
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0a6b769a-c66d-4784-92c7-a70caa28e1ba",
              "name": "item",
              "type": "object",
              "value": "={{ $node[\"ForEach\"].json }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bca5e2c4-7aa3-48df-9e5f-b31977970c28",
      "name": "SetDataCreateNode",
      "type": "n8n-nodes-base.set",
      "position": [
        1220,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0a6b769a-c66d-4784-92c7-a70caa28e1ba",
              "name": "item",
              "type": "object",
              "value": "={{ $node[\"ForEach\"].json }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bf74b1ea-e066-462b-9c3d-ed4a44a09a33",
      "name": "Base64EncodeUpdate",
      "type": "n8n-nodes-base.code",
      "position": [
        2140,
        240
      ],
      "parameters": {
        "language": "python",
        "pythonCode": "import json
import base64
from js import Object

# Assuming _input.all() returns a JavaScript object
js_object = _input.all()

# Convert the JsProxy object to a Python dictionary
def js_to_py(js_obj):
    if isinstance(js_obj, (str, int, float, bool)) or js_obj is None:
        # Base types are already Python-compatible
        return js_obj
    elif isinstance(js_obj, list):
        # Convert lists recursively
        return [js_to_py(item) for item in js_obj]
    elif hasattr(js_obj, \"__iter__\") and not isinstance(js_obj, str):
        # Handle JsProxy objects (JavaScript objects or arrays)
        if hasattr(js_obj, \"keys\"):
            # If it has keys, treat it as a dictionary
            return {key: js_to_py(js_obj[key]) for key in Object.keys(js_obj)}
        else:
            # Otherwise, treat it as a list
            return [js_to_py(item) for item in js_obj]
    else:
        # Fallback for other types
        return js_obj

# Convert the JavaScript object to a Python dictionary
input_dict = js_to_py(js_object)

# Step 0: get the correct data set of the workflow
inner_data = input_dict[0].get('json').get('item')

# Step 1: Convert the dictionary to a pretty-printed JSON string
json_string = json.dumps(inner_data, indent=4)

# Step 2: Encode the JSON string to bytes
json_bytes = json_string.encode('utf-8')

# Step 3: Convert the bytes to a base64 string
base64_string = base64.b64encode(json_bytes).decode('utf-8')

# Step 5: Create the return object with the base64 string and its SHA-256 hash
return_object = {
    \"item\": base64_string
}

# Return the object
return return_object"
      },
      "typeVersion": 2
    },
    {
      "id": "2d817c66-5aa0-45c9-b851-4b5e3dbecca4",
      "name": "Base64EncodeCreate",
      "type": "n8n-nodes-base.code",
      "position": [
        1520,
        640
      ],
      "parameters": {
        "language": "python",
        "pythonCode": "import json
import base64
from js import Object

# Assuming _input.all() returns a JavaScript object
js_object = _input.all()

# Convert the JsProxy object to a Python dictionary
def js_to_py(js_obj):
    if isinstance(js_obj, (str, int, float, bool)) or js_obj is None:
        # Base types are already Python-compatible
        return js_obj
    elif isinstance(js_obj, list):
        # Convert lists recursively
        return [js_to_py(item) for item in js_obj]
    elif hasattr(js_obj, \"__iter__\") and not isinstance(js_obj, str):
        # Handle JsProxy objects (JavaScript objects or arrays)
        if hasattr(js_obj, \"keys\"):
            # If it has keys, treat it as a dictionary
            return {key: js_to_py(js_obj[key]) for key in Object.keys(js_obj)}
        else:
            # Otherwise, treat it as a list
            return [js_to_py(item) for item in js_obj]
    else:
        # Fallback for other types
        return js_obj

# Convert the JavaScript object to a Python dictionary
input_dict = js_to_py(js_object)

# Step 0: get the correct data set of the workflow
inner_data = input_dict[0].get('json').get('item')

# Step 1: Convert the dictionary to a pretty-printed JSON string
json_string = json.dumps(inner_data, indent=4)

# Step 2: Encode the JSON string to bytes
json_bytes = json_string.encode('utf-8')

# Step 3: Convert the bytes to a base64 string
base64_string = base64.b64encode(json_bytes).decode('utf-8')

# Step 4: Create the return object with the base64 string in 'item'
return_object = {
    \"item\": base64_string
}

# Return the object
return return_object"
      },
      "typeVersion": 2
    },
    {
      "id": "41a7da89-1c8c-4100-8c30-d0788962efc1",
      "name": "Exist",
      "type": "n8n-nodes-base.if",
      "position": [
        1640,
        260
      ],
      "parameters": {
        "options": {
          "ignoreCase": false
        },
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "16a9182d-059d-4774-ba95-654fb4293fdb",
              "operator": {
                "type": "object",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.error }}",
              "rightValue": 404
            }
          ]
        }
      },
      "executeOnce": false,
      "typeVersion": 2.2,
      "alwaysOutputData": false
    },
    {
      "id": "ab9246eb-a253-4d76-b33b-5f8f12342542",
      "name": "Changed",
      "type": "n8n-nodes-base.if",
      "position": [
        2360,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e0c66624-429a-4f1f-bf7b-1cc1b32bad7b",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.item }}",
              "rightValue": "={{ $('GetGitea').item.json.content }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "4278a176-6496-4817-82f8-591539619673",
      "name": "PutGitea",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2700,
        360
      ],
      "parameters": {
        "url": "={{ $('Globals').item.json.repo.url }}/api/v1/repos/{{ $('Globals').item.json.repo.owner }}/{{ $('Globals').item.json.repo.name }}/contents/{{ encodeURIComponent($('GetGitea').item.json.name) }}",
        "method": "PUT",
        "options": {},
        "sendBody": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "content",
              "value": "={{ $('Base64EncodeUpdate').item.json.item }}"
            },
            {
              "name": "sha",
              "value": "={{ $('GetGitea').item.json.sha }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "gTvBAgkOmqhl5Nmr",
          "name": "Gitea Token"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "12307a61-e7cc-42f9-a7c7-8abbcab9e3ab",
      "name": "GetGitea",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1380,
        260
      ],
      "parameters": {
        "url": "={{ $('Globals').item.json.repo.url }}/api/v1/repos/{{ encodeURIComponent($('Globals').item.json.repo.owner) }}/{{ encodeURIComponent($('Globals').item.json.repo.name) }}/contents/{{ encodeURIComponent($json.name) }}.json",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "gTvBAgkOmqhl5Nmr",
          "name": "Gitea Token"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "24fda439-bb23-4392-a297-d8070907f9e6",
      "name": "PostGitea",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1920,
        640
      ],
      "parameters": {
        "url": "={{ $('Globals').item.json.repo.url }}/api/v1/repos/{{ $('Globals').item.json.repo.owner }}/{{ $('Globals').item.json.repo.name }}/contents/{{ encodeURIComponent($('ForEach').item.json.name) }}.json",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "content",
              "value": "={{ $json.item }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "gTvBAgkOmqhl5Nmr",
          "name": "Gitea Token"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "43a60315-d381-4ac4-be4c-f6a158651a00",
      "name": "ForEach",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1060,
        240
      ],
      "parameters": {
        "options": {}
      },
      "executeOnce": false,
      "typeVersion": 3
    },
    {
      "id": "88578dc4-2398-48d0-b0ba-2198b35bb994",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        380,
        440
      ],
      "parameters": {
        "width": 560,
        "height": 1620,
        "content": "### **📌 Setup Guide for Backup Workflows to Git Repository on Gitea**

#### **🔧 1. Configure Global Variables**
Go to the **Globals** node and update the following:
- **`repo.url`** → `https://your-gitea-instance.com` *(Replace with your actual Gitea URL)*
- **`repo.name`** → `workflows` *(Repository name where backups will be stored)*
- **`repo.owner`** → `octoleo` *(Gitea account that owns the repository)*

📌 **These settings define where workflows will be backed up.**

---

#### **🔑 2. Set Up Gitea Authentication**
1️⃣ **In Gitea:**
- Generate a **Personal Access Token** under **Settings → Applications → Generate Token**
- Ensure the token has **repo read/write permissions**

2️⃣ **In the Credentials Manager:**
- Create a new **Gitea Token** credential
- Set the **Name** as `Authorization`
- Set the **Value** as:
```
Bearer YOUR_PERSONAL_ACCESS_TOKEN
```
📌 **Ensure there is a space after `Bearer` before the token!**

---

#### **🔗 3. Connect Gitea Credentials to Git Nodes**
- Open each of these **three Git nodes**:
- **GetGitea** → Retrieves existing repository data
- **PutGitea** → Updates workflows
- **PostGitea** → Adds new workflows

- Assign the **Gitea Token** credential to each node.

📌 **These nodes handle pushing your workflows to Gitea.**

---

#### **🌐 4. Set Up API Credentials for Workflow Retrieval**
- Locate the API request node that **fetches workflows**.
- Add your **API authentication credentials** (Token or Basic Auth).

📌 **This ensures the workflow can fetch all available workflows from your system.**

---

#### **🛠️ 5. Test & Activate the Workflow**
✅ **Run the workflow manually** → Check that workflows are being backed up correctly.
✅ **Review the Gitea repository** → Ensure the files are updated.
✅ **Enable the scheduled trigger** → Automates backups at defined intervals.

📌 **The workflow automatically checks for changes before committing updates!**

---

### **🚀 Done! Your Workflows Are Now Backed Up Securely!**
💬 Have issues? **Reach out on the forum for help!**"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "84ba3f3f-fbc8-4792-8e28-198f515fef4e",
  "staticData": {
    "node:Schedule Trigger": {
      "recurrenceRules": []
    }
  },
  "connections": {
    "n8n": {
      "main": [
        [
          {
            "node": "ForEach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Exist": {
      "main": [
        [
          {
            "node": "SetDataUpdateNode",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "SetDataCreateNode",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Changed": {
      "main": [
        [
          {
            "node": "PutGitea",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ForEach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ForEach": {
      "main": [
        [],
        [
          {
            "node": "GetGitea",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Globals": {
      "main": [
        [
          {
            "node": "n8n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GetGitea": {
      "main": [
        [
          {
            "node": "Exist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PutGitea": {
      "main": [
        [
          {
            "node": "ForEach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PostGitea": {
      "main": [
        [
          {
            "node": "ForEach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Globals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SetDataCreateNode": {
      "main": [
        [
          {
            "node": "Base64EncodeCreate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SetDataUpdateNode": {
      "main": [
        [
          {
            "node": "Base64EncodeUpdate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Base64EncodeCreate": {
      "main": [
        [
          {
            "node": "PostGitea",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Base64EncodeUpdate": {
      "main": [
        [
          {
            "node": "Changed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "triggerCount": 1
}

功能特点

  • 自动检测新邮件
  • AI智能内容分析
  • 自定义分类规则
  • 批量处理能力
  • 详细的处理日志

技术分析

节点类型及作用

  • Set
  • N8N
  • Scheduletrigger
  • Stickynote
  • Code

复杂度评估

配置难度:
★★★★☆
维护难度:
★★☆☆☆
扩展性:
★★★★☆

实施指南

前置条件

  • 有效的Gmail账户
  • n8n平台访问权限
  • Google API凭证
  • AI分类服务订阅

配置步骤

  1. 在n8n中导入工作流JSON文件
  2. 配置Gmail节点的认证信息
  3. 设置AI分类器的API密钥
  4. 自定义分类规则和标签映射
  5. 测试工作流执行
  6. 配置定时触发器(可选)

关键参数

参数名称 默认值 说明
maxEmails 50 单次处理的最大邮件数量
confidenceThreshold 0.8 分类置信度阈值
autoLabel true 是否自动添加标签

最佳实践

优化建议

  • 定期更新AI分类模型以提高准确性
  • 根据邮件量调整处理批次大小
  • 设置合理的分类置信度阈值
  • 定期清理过期的分类规则

安全注意事项

  • 妥善保管API密钥和认证信息
  • 限制工作流的访问权限
  • 定期审查处理日志
  • 启用双因素认证保护Gmail账户

性能优化

  • 使用增量处理减少重复工作
  • 缓存频繁访问的数据
  • 并行处理多个邮件分类任务
  • 监控系统资源使用情况

故障排除

常见问题

邮件未被正确分类

检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。

Gmail认证失败

确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。

调试技巧

  • 启用详细日志记录查看每个步骤的执行情况
  • 使用测试邮件验证分类逻辑
  • 检查网络连接和API服务状态
  • 逐步执行工作流定位问题节点

错误处理

工作流包含以下错误处理机制:

  • 网络超时自动重试(最多3次)
  • API错误记录和告警
  • 处理失败邮件的隔离机制
  • 异常情况下的回滚操作